feat: version range support for package dependencies#491
Draft
feat: version range support for package dependencies#491
Conversation
Support caret (^) and tilde (~) version range operators in mops.toml dependencies, similar to npm and Cargo. - `mops add` now defaults to caret range (e.g., core = "^1.2.3") - Ranges resolved to highest satisfying version during install - Lock file stores exact resolved versions for reproducibility - Backend validates ^/~ prefixed versions in published configs - New getPackageVersions API endpoint for client-side resolution - Resolver validates all range constraints are satisfied post-resolution Made-with: Cursor
- Reduce cli/semver.ts from 135 to 14 lines (thin wrapper with isRange/stripRangePrefix) - Use semver.maxSatisfying, semver.satisfies, semver.compare from the already-installed semver@7.7.1 - Cache directory listing in findCachedVersions to avoid N redundant readdirSync calls - Remove redundant stripRangePrefix call on exact deps in available-updates.ts - Slim tests from 202 to 47 lines (only test mops-specific helpers, not npm semver semantics) Made-with: Cursor
- Use lock file resolved version (not range floor) when checking if ranged deps have available updates - Add resetCachedDirEntries() and invalidate before resolvePackages to ensure freshly installed packages are visible Made-with: Cursor
Made-with: Cursor
…ranges The existing getHighestSemverBatch endpoint already covers range resolution — rangeToSemverPart maps ^/~ to the correct SemverPart (#major, #minor, #patch). This eliminates a new backend endpoint, the N+1 call problem, and the dedicated API wrapper. Made-with: Cursor
The SemverPart mapping was inverted — #major means "any higher" not "same major". Fixed: ^X.Y.Z→#minor, ~X.Y.Z→#patch, ^0.0.x→null (exact pin). Removed all backend canister changes (semver.mo, validateConfig.mo) except the "fromat" typo fix. publish.ts now strips range prefixes before sending config to the backend, so no canister upgrade needed. Also: fix ESLint/Prettier violations, dedupe update.ts loop, remove low-info comments, explicit error on empty batch result. Made-with: Cursor
Made-with: Cursor
- Drop module-level cache state in cli/cache.ts; replace findCachedVersions +
resetCachedDirEntries with stateless listCachedPackages
- Inline range resolution in resolvePackages; comparison uses stripRangePrefix
+ semver.compare directly (no cache lookup, no non-null assertion)
- Run resolver's full graph walk when conflicts != "ignore" so the
range-satisfaction check actually fires after mops add/update
- mops add core@^x.y.z now passes the range to installMopsDep (was installing
the floor instead of highest matching)
- rangeToSemverPart returns null on invalid input instead of silently
defaulting to {minor: null}
- Skip per-constraint check when major-mismatch was already reported
- Warn (not reject) on publish with ranged deps; older CLI versions cannot
install such packages
- Restore unrelated comments to reduce churn
Made-with: Cursor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Status: paused — exploring alternatives
This PR is on hold while we explore a less invasive design (see "Why we're pausing" below). Pushing the current state for posterity and so others can refer to it.
Problem
Mops currently requires exact version pins for all dependencies (e.g.
core = "1.2.3"). Library authors cannot express compatibility constraints like "my library works with anycore 1.x >= 1.2.3", and application developers must manually bump every transitive dependency version — there is no way formops installto automatically pick up compatible patch/minor updates.What this PR does
Adds caret (
^) and tilde (~) version range operators, following npm/Cargo conventions:^1.2.3=>=1.2.3, <2.0.0~1.2.3=>=1.2.3, <1.3.0^0.2.3=>=0.2.3, <0.3.0CLI behavior:
mops add corewritescore = "^x.y.z"(caret default)mops add core@1.2.3writes exact pin (unchanged)mops updatepreserves range type and bumps the lower boundWhy we're pausing
Two issues surfaced during review that make this design more disruptive than it first looked.
1. Backend canister doesn't accept ranges
backend/main/utils/validateConfig.moruns strictSemver.validate(x.y.zonly) on every dependency in a published package's config, andPackagePublisher.modoes an exactregistry.getPackageConfig(name, dep.version)lookup. A package published withcore = "^1.2.3"is rejected by the canister at publish time.The CLI prints a warning, but the publish still fails — the warning is misleading. Properly supporting ranges in published packages requires backend changes (relaxed validation + range-aware lookup), which means coordinating a canister upgrade.
2. The breaking-change surface is wider than expected
Even with the publish path fixed, every consumer reading a published
mops.tomlcontaining^/~needs a new-enough CLI to interpret it. Old CLIs would treat^1.2.3as an exact version literal, fail to find it, and break. This is a registry-wide one-way door: the moment any popular package adopts ranges, it forks the ecosystem by CLI version.There is no good way to gate this behind an opt-in flag either, because the on-wire format change propagates to every downstream consumer.
3. Mops already does "compatible-ish" resolution implicitly
Today, when two packages depend on different exact versions of the same dep, the resolver picks the highest one. Effectively most users already get caret-like behavior for transitive deps without writing
^. The marginal value of explicit^/~syntax is smaller than expected, and concentrated on letting library authors narrow (with~) rather than widen the contract — a less common need.Alternative being explored
A separate PR will explore a Cargo-inspired approach hidden behind an opt-in experimental flag in
mops.toml:When enabled, bare versions in the consumer's own
mops.tomlare interpreted as caret ranges at resolve time. Crucially:mops.toml(no^/~)This is a much narrower experiment that targets the same outcome (automatic compatible patch/minor pickup) without the registry-wide blast radius. See follow-up PR (TBD).
What's salvageable from this PR
semverpackage integration andcli/semver.tshelpersThese can be extracted into smaller PRs independent of the range-syntax decision.
Test plan
isRange,stripRangePrefix,rangeToSemverPart